package com.joyrec.util.db.mongo

import scala.annotation.tailrec
import scala.collection.convert.WrapAsScala
import scala.collection.convert.WrapAsJava
import com.mongodb.BasicDBObject
import com.mongodb.DBCollection
import com.mongodb.DBObject
import com.mongodb.Mongo
import com.mongodb.BasicDBList
import ws.very.util.lang.Scala2JavaColl
import com.mongodb.WriteConcern
import scala.collection.Iterator
import scala.annotation.StaticAnnotation
import ws.very.util.entity.HasId
import ws.very.util.lang.ImplicitCookies
import scala.util.Try
import java.util.Date
import ws.very.util.lang.StopWatch
import com.mongodb.BasicDBObjectBuilder

trait MongoTpl {

  protected def reader: Mongo
  protected def writer: Mongo
}

trait MOable extends ImplicitCookies {
  protected final val _id = "_id"
  protected final val $set = "$set"
  protected final val $inc = "$inc"
  protected final val $in = "$in"
  protected final val $regex = "$regex"
  protected final val $and = "$and"
  protected final val $or = "$or"
  protected final val $exists = "$exists"
  protected final val $push = "$push"
  protected final val $pushAll = "$pushAll"
  protected final val $pullAll = "$pullAll"
  protected final val $pull = "$pull"
  protected final val $ne = "$ne"
  protected final val $match = "$match"
  protected final val $sort = "$sort"
  protected final val $group = "$group"
  protected final val $first = "$first"
  protected final val $sum = "$sum"
  protected final val $skip = "$skip"
  protected final val $limit = "$limit"
  protected final val justId = MO(_id -> 1)

  protected final val NullList = new BasicDBList //FIXME:uq
  protected final val NullObj = new BasicDBObject //FIXME:uq

  protected val modifyTimeField = "m"

  protected def pathIn[T <% Iterable[Any]](path: String, ids: T) =
    MO(path -> MO($in -> ids))
  protected def idIn[T <% Iterable[Any]](ids: T) = pathIn(_id, ids) //FIXME:约束性
  protected def idStartsWith(start: String) =
    pathStartsWith(_id, start)

  protected def pathStartsWith(path: String, start: String) =
    MO(path -> MO($regex -> ("^" + start)))

  protected object MO {
    private val replacer = Map(">" -> "$gt", //
      "<" -> "$lt", //
      ">=" -> "$gte", //
      "<=" -> "$lte", //
      "!=" -> "$ne")
    protected class MOImpl extends BasicDBObject {
      override def put(key: String, v: Object) =
        super.put(replacer.getOrElse(key, key), Scala2JavaColl(v))
      def +(kv: (String, Object)): this.type = {
        put(kv._1, kv._2)
        this
      }

      def ++(kvs: Iterable[(String, Object)]) = {
        kvs.map { case (k, v) => put(k, v) }
        this
      }
    }
    def apply(params: (String, Any)*) = {
      val mo = new MOImpl
      params.foreach {
        case (k, v) =>
          mo.put(k, v.asInstanceOf[Object])
      }
      mo
    }

    //    def applz(params: (String, Any)*) = {
    //      val mo = new MOImpl
    //      params.foreach {
    //        case (k, Some(v)) =>
    //          mo.put(k, v.asInstanceOf[Object])
    //        case (k, None) => mo
    //        case (k, v) =>
    //          mo.put(k, v.asInstanceOf[Object])
    //      }
    //      println(mo)
    //      mo
    //    }

  }

  protected implicit class MoExpand2(obj: BasicDBObject) extends MoExpand(obj) with WrapAsScala {
    def getOrUpdate[R, D <: R](f: String, update: D): R =
      obj.getOrElseUpdate(f, update.asInstanceOf[Object]).asInstanceOf[R]
    def setBy(path: String, value: Any) =
      {
        val ps = path.split("\\.")
        getUpdateBy(obj, ps.init: _*).getOrUpdate(ps.last, value)
      }

    @tailrec
    private def getUpdateBy(cur: Any, paths: String*): BasicDBObject =
      cur match {
        case dbObject: BasicDBObject if (paths.nonEmpty) =>
          getUpdateBy(dbObject.getOrUpdate(paths.head, new BasicDBObject), paths.tail: _*)
        case other => other.asInstanceOf[BasicDBObject]
      }
  }

  protected implicit class MoExpand[T <: DBObject](mo: T) {
    def getFieldOption[R](field: String) =
      Option(mo.get(field).asInstanceOf[R])
    def getOrElse[R, T <: R](f: String, default: T) =
      getFieldOption(f).getOrElse(default).asInstanceOf[R]

    def getOrNullObj(f: String) =
      getOrElse(f, NullObj)

    def id[T] = getOrEx[T]("_id")

    def getOrEx[T](f: String) =
      getFieldOption(f).getOrElse(throw new RuntimeException(s"field:$f is null")).asInstanceOf[T]

    def getOrNil(f: String) =
      getOrElse(f, NullList)

    def getByOrNil(p: String) =
      getByOrElse(p, NullList)

    def getByOrNObj(p: String) = getByOrElse(p, NullObj)
    def getBy[R](path: String): R =
      byPath(path).asInstanceOf[R]

    def getByOrElse[R, T <: R](path: String, default: T): R =
      getByOption(path).getOrElse(default).asInstanceOf[R]

    def getByOrEx[R](path: String) =
      getByOption(path).getOrElse(throw new RuntimeException(s"path:$path is null")).asInstanceOf[R]
     def byPath(path: String) = getBy(mo, path.split("\\."): _*)

    def getByOption[R](path: String) = Option(byPath(path).asInstanceOf[R])

    @tailrec
    private def getBy(cur: Any, paths: String*): Any =
      cur match {
        case dbObject: DBObject if (paths.nonEmpty) => getBy(dbObject.get(paths.head), paths.tail: _*)
        case other => other
      }
    def withTimeField(f: String = modifyTimeField) = {
      getOrElse($set, mo).put(f, System.currentTimeMillis)
      mo
    }
  }

}

trait MongoTTLCollTpl extends MongoCollTpl {
  protected val ttlField = "~t"
  protected val expireAfterSeconds: Int = 24 * 60 * 60
  private def withTTLUpdate(action: DBObject, time: Date = new Date) = {
    val set = action.get($set)
    val newSet = if (set == null) {
      val set = MO()
      action.put($set, set)
      set
    } else set.asInstanceOf[DBObject]
    newSet.put(ttlField, time) //fixme:$set
    action
  }

  def withTTL(sort: Int = 1) =
    ensureIndex(MO(ttlField -> sort), MO("expireAfterSeconds" -> expireAfterSeconds))

  def updateWithTTL(mo: DBObject, action: DBObject, time: Date = new Date, upsert: Boolean = true, multi: Boolean = true) =
    update(mo, withTTLUpdate(action, time), upsert, multi)

  def updateOneWithTTL(mo: DBObject, action: DBObject, time: Date = new Date, upsert: Boolean = true) =
    updateOne(mo, withTTLUpdate(action, time), upsert)

  def updateTTLById(id: Any, upsert: Boolean = false) =
    updateById(id, withTTLUpdate(MO()), upsert)
}

trait BasicMongoCollTpl extends MongoCollTpl

/*FIXME:耦合*/
trait MongoCollTpl extends MongoTpl with MOable with WrapAsJava with WrapAsScala {
  def ensureIndex(keys: DBObject, options: DBObject = MO()) =
    writeColl.ensureIndex(keys, options)

  override def toString = s"tpl 4 coll $coll on DB $db"
  protected val db: String
  protected val coll: String
  protected val collOptions: DBObject
  protected def readColl: DBCollection = reader.getDB(db).getCollection(coll)
  protected def writeColl: DBCollection = {
    val writeDB = writer.getDB(db)
    if (writeDB.collectionExists(coll))
      writeDB.getCollection(coll)
    else
      writeDB.createCollection(coll, collOptions)
  }
  def find(find: DBObject, select: DBObject = MO(), sortBy: DBObject = MO(), limit: Int = 0, batchSize: Int = 0, options: Iterable[Int] = Iterable()) =
    asScalaIterator {
      val t = readColl.find(find, select).sort(sortBy)
      options.foreach { o =>
        t.addOption(o)
      }
      if (limit > 0)
        t.limit(limit)
      if (batchSize > 0)
        t.batchSize(batchSize)
      t
    }

  def find(field: String, eqValue: Any): Iterator[com.mongodb.DBObject] =
    find(MO(field -> eqValue))

  def aggregate(moHead: DBObject, moOthers: DBObject*) =
    null //readColl.aggregate(moHead, moOthers: _*)

  def count(where: DBObject) =
    readColl.count(where)

  def findIdIn[T <% Iterable[Any]](ids: T, select: DBObject = MO(),
    sortBy: DBObject = MO(), limit: Int = 0, batchSize: Int = 0, options: Iterable[Int] = Iterable()) =
    find(idIn(ids), select, sortBy, limit, batchSize, options)

  def find2MapByIds[T <% Iterable[Any]](ids: T) = {
    val map = findIdIn(ids).map { t => (t.id[Any], t) }.toMap
    ids.map { t =>
      (t, map.getOrElse(t, null))
    }.toMap
  }

  def incValById(id: String, field: String = "v", inc: Int = 1): Long = {
    val mo = writeColl.findAndModify(MO(_id -> id),
      MO(field -> 1, _id -> 0), MO(), false, MO($inc -> MO(field -> 1)), false, true)
    if (mo == null || mo.getFieldOption(field) == null)
      incValById(id, field, inc)
    else mo.getOrEx[Number](field).longValue()
  }

  def findAll =
    asScalaIterator(readColl.find)

  def findOne(find: DBObject, select: DBObject = MO(), sortBy: DBObject = MO()): Option[com.mongodb.DBObject] =
    Option(readColl.findOne({
      find
    }, select, sortBy))

  def findById(id: Any, select: DBObject) =
    findOne(MO(_id -> id), select)

  def distinct[T](key: String, query: DBObject = MO()) =
    readColl.distinct(key, query).map(_.asInstanceOf[T])

  def insert(mo: DBObject) =
    writeColl.insert(mo)

  def insert(mos: Iterable[DBObject], continueOnError: Boolean = false) =
    if (!continueOnError)
      writeColl.insert(mos.toList)
    else {
      writeColl.setWriteConcern(writeColl.getWriteConcern().continueOnErrorForInsert(true))
      writeColl.insert(mos.toList, writeColl.getWriteConcern)
    }
  def findOne(field: String, eqValue: Any): Option[com.mongodb.DBObject] =
    findOne(MO(field -> eqValue))

  def findById(id: Any) =
    findOne(_id, id)

  def update(query: DBObject, action: DBObject, upsert: Boolean = true, multi: Boolean = true) =
    writeColl.update(query, action, upsert, multi)

  def updateOne(query: DBObject, action: DBObject, upsert: Boolean = true) =
    update(query, action, upsert, false)

  def updateById(id: Any, action: DBObject, upsert: Boolean = true) =
    updateOne(MO(_id -> id), action, upsert)

  def updateIdIn[T <: Iterable[Any]](ids: T, updat: DBObject, upsert: Boolean = false, multi: Boolean = true) =
    update(idIn(ids), updat, upsert, multi)

  def delete(find: DBObject) = //FIXME:有返回删除数吗
    writeColl.remove(find)

  def deleteByIds(ids: Iterable[Any]) =
    delete(MO(_id -> MO($in -> ids)))

  def deleteById(id: Any) =
    delete(MO(_id -> id))

  def save(mo: DBObject) =
    writeColl.save(mo)

  def findLimits(query: DBObject, limit: Int) =
    readColl.find(query).limit(limit)

  def exists(field: String) =
    findOne(MO(field -> MO("$exists" -> true)), MO(_id -> 1)) match {
      case None => false
      case _ => true
    }

}

trait MongoGraph extends MOable with WrapAsScala {

  object Direction extends Enumeration {
    val Out, In, Both = Value
  }

  object Query extends Enumeration {
    val In, StratsWith, >, <, ==, !=, >=, <= = Value
  }

  type Query = Query.Value
  import Query._

  protected def nodeTpl: MongoCollTpl
  protected def relationshipTpl: MongoCollTpl
  //  protected def debugLogTpl: MongoCollTpl

  trait MultOpsable[T <: SingleOpsable] extends Opsable with HasTpl with Iterable[T] with Updateable {
    protected def coll: Iterable[T]
    def del: this.type = {
      tpl.deleteByIds(this.map { t => t.id })
      this
    }
    def save: this.type = {
      this.foreach { m => m.save }
      this
    }
    def insert: this.type = {
      tpl.insert(this.withFilter { t: T => (t.isDataExists == None || !(t.isDataExists.get)) }.map { _.factData })
      this
    }
    def iterator: Iterator[T] = coll.iterator

    def update(upsert: Boolean): this.type = {
      tpl.updateIdIn(this.map(_.id), updateMo, upsert)
      this
    }
  }
  implicit class RelationshipIter(val coll: Iterable[Relationship]) extends MultOpsable[Relationship] with RSTpl {
    def endNodes: NodeIter =
      coll.map { _.endNode }
    def startNodes: NodeIter =
      coll.map { _.startNode }
    def nodes: NodeIter =
      coll.flatMap { t =>
        val (n1, n2) = t.nodes
        Array(n1, n2)
      }
    def otherNodes(node: Node): NodeIter =
      nodes.filterNot(_ == node)
  }

  implicit class NodeIter(val coll: Iterable[Node]) extends MultOpsable[Node] with NodeTpl {
    def initAll = {
      val ids = coll.withFilter { n => n.isInitd == false }.map { n => n.id }
      val map = tpl.find2MapByIds(ids)
      coll.map { n =>
        if (map.contains(n.id))
          if (map.get(n.id) != None) Node(map.get(n.id).get)
          else Node(n.id, false)
        else n
      }
    }
  }

  trait RSTpl extends HasTpl {
    protected def tpl: MongoCollTpl = relationshipTpl
  }

  trait NodeTpl extends HasTpl {
    protected def tpl: MongoCollTpl = nodeTpl
  }
  trait Opsable {
    def del: this.type
    def save: this.type
    def insert: this.type

  }

  trait Attrable {
    def apply[T](attr: String): T
    def update(attr: String, value: Any): this.type
    def isNonEmpty: Boolean
  }

  trait HasMoData {
    def isInitd: Boolean
    def isDataExists: Option[Boolean]
    protected def defaultMO: BasicDBObject
    def factData: BasicDBObject
  }

  trait Updateable extends HasTpl with MOable {
    protected lazy val updateMo = MO()
    def set(path: String, value: Any): this.type = {
      updateMo.getOrUpdate($set, MO())(path) = value.asInstanceOf[Object]
      this
    }

    def inc(path: String, value: Int): this.type = {
      updateMo.getOrUpdate($inc, MO())(path) = value.asInstanceOf[Object]
      this
    }
    def push(path: String, value: Any): this.type = {
      updateMo.getOrUpdate($push, MO())(path) = value.asInstanceOf[Object]
      this
    }
    def pushAll(path: String, value: Iterable[Any]): this.type = {
      updateMo.getOrUpdate($pushAll, MO())(path) = value.asInstanceOf[Object]
      this
    }
    def pull(path: String, value: Any): this.type = {
      updateMo.getOrUpdate($pull, MO())(path) = value.asInstanceOf[Object]
      this
    }
    def pullAll(path: String, value: Iterable[Any]): this.type = {
      updateMo.getOrUpdate($pullAll, MO())(path) = value.asInstanceOf[Object]
      this
    }
    def syncTimeStamp(path: String, time: Long = System.currentTimeMillis()): this.type = {
      updateMo.getOrUpdate($set, MO())(path) = time.asInstanceOf[Object]
      this
    }

    def update(upsert: Boolean = true): this.type
  }

  trait LazyMoData extends HasTpl with HasId[String] with HasMoData {
    var isInitd = false
    var isDataExists: Option[Boolean] = None
    protected val defaultMO: BasicDBObject
    lazy val factData = {
      val r = tpl.findById(id) match {
        case Some(v) =>
          isDataExists = Some(true); v.asInstanceOf[BasicDBObject]
        case None => isDataExists = Some(false); defaultMO
      }
      isInitd = true
      r
    }
  }
  trait IntimeMoData extends HasMoData {
    def mo: DBObject
    val id = mo.id[String]
    val factData = mo.asInstanceOf[BasicDBObject]
    val isDataExists = Some(true)
    val isInitd = true
  }

  trait NoExistsMoData extends HasMoData {

    val isDataExists = Some(false)
    val isInitd = true
    val factData = defaultMO
  }

  trait AttrableImpl extends Attrable with HasMoData {

    def apply[T](attr: String): T =
      factData.getByOrEx(attr)
    def update(attr: String, value: Any): this.type = {
      factData.setBy(attr, value)
      this
    }
    def isEmpty = !isNonEmpty
    def isNonEmpty = factData.keySet().exists(_ != _id)
  }

  trait SingleOpsable extends Opsable with HasMoData with HasId[String] with HasTpl with Updateable {
    def del: this.type = {
      tpl.deleteById(id)
      this
    }
    def save: this.type = {
      if (isInitd)
        tpl.save(factData)
      else tpl.save(defaultMO)
      this
    }
    def insert: this.type = {
      if (isDataExists != None && isDataExists.get)
        println(s"$id is exists!")
      else
        tpl.insert(defaultMO)
      this
    }

    def update(upsert: Boolean): this.type = {
      tpl.update(MO((_id, id)), updateMo)
      this
    }
  }

  trait HasTpl {
    protected def tpl: MongoCollTpl
  }

  trait Nodeable {
    type Dir = Direction.Value
    def relationshipTo(rsType: String, nodes: NodeIter): RelationshipIter
    def relationshipTo(rsType: String, node: Node): Relationship
    def relationships(dir: Dir = Direction.Both, types: Iterable[String] = Nil, endNodeAndQuery: Iterable[(String, Query, Any)] = Nil, soryBy: Iterable[(String, Boolean)] = Nil, limit: Int = 0, skip: Int = 0): RelationshipIter

    def singleRelationship(typ: String, dir: Dir = Direction.Both): Option[Relationship]
    def hasRelationship(dir: Dir = Direction.Both, types: Iterable[String] = Nil): Boolean
    //    def countRelationShip(dir: Dir = Direction.Both, types: Iterable[String] = Nil): Int
  }

  trait NodeableImpl extends Node with SingleOpsable with AttrableImpl with NodeTpl {
    lazy val defaultMO = MO(_id -> id)
    def relationshipTo(rsType: String, nodes: NodeIter): RelationshipIter =
      (nodes.map { n =>
        Relationship(this, rsType, n)
      })

    def relationshipTo(rsType: String, node: Node): Relationship = Relationship(this, rsType, node)

    // val In, StratsWith, >, <, ==, !=, >=, <= = Value
    protected def query(dir: Dir = Direction.Both, types: Iterable[String] = Nil,
      endNodeIdAndQuery: Iterable[(Query, Any)] = Nil) = {

      (dir, types.nonEmpty, endNodeIdAndQuery.nonEmpty) match {
        case (Direction.Out, true, false) =>
          MO($or -> types.map { t => idStartsWith(Relationship.RightId.init(id, t)) })
        case (Direction.Out, true, true) =>
          MO($and -> endNodeIdAndQuery.map {
            case (In, ids: Iterable[Any]) => idIn(
              types.flatMap { t =>
                ids.map { tid =>
                  Relationship.RightId(id, t, tid.toString)
                }
              })
            case (==, tid) =>
              idIn(types.map { t =>
                Relationship.RightId(id, t, tid.toString)
              })
            case (!=, tid) =>
              MO($or -> types.map { t =>
                MO($and ->
                  Array(
                    Relationship.RightId.init(id, t),
                    MO(_id -> MO($ne -> Relationship.RightId(id, t, tid.toString)))))
              })
            case (StratsWith, start) =>
              MO($or -> types.map { t =>
                idStartsWith(Relationship.RightId(id, t, start.toString))
              })
            case (op, value) if op == > || op == >= || op == < || op == <= =>
              MO($or -> types.map { t =>
                MO($and -> Array(idStartsWith(Relationship.RightId.init(id, t)), MO(_id -> MO((op match {
                  case > => "$gt"
                  case >= => "$gte"
                  case < => "$lt"
                  case <= => "$lte"
                }) -> value))))
              })
            // case 
          })
        case (Direction.Out, false, false) =>
          idStartsWith(Relationship.RightId.head(id))

        case (Direction.In, true, false) =>
          MO("e" -> MO($in -> types.map { t => Relationship.LeftId(id, t) }))
        case (Direction.In, false, false) =>
          pathStartsWith("e", Relationship.LeftId.head(id))
        case (Direction.Both, true, false) =>
          MO($or -> types.flatMap { t => Array(idStartsWith(Relationship.RightId.init(id, t)), MO("e" -> Relationship.LeftId(id, t))) })
        case (Direction.Both, false, false) =>
          MO($or -> Array(idStartsWith(Relationship.RightId.head(id)), pathStartsWith("e", Relationship.LeftId.head(id))))

      }
    }

    def relationships(dir: Dir = Direction.Both, types: Iterable[String] = Nil, endNodeIdQuery: Iterable[(String, Query, Any)] = Nil, soryBy: Iterable[(String, Boolean)] = Nil, limit: Int = 0, skip: Int = 0): RelationshipIter =
      new RelationshipIter(relationshipTpl.find(query(dir = dir, types = types, endNodeIdAndQuery = Nil), MO(_id -> 1)).map { Relationship(_) }.toStream)

    def singleRelationship(typ: String, dir: Dir = Direction.Both) =
      relationshipTpl.findOne(query(dir, Array(typ))).map { Relationship(_) }
    def hasRelationship(dir: Dir = Direction.Both, types: Iterable[String] = Nil) =
      relationshipTpl.findOne(query(dir, types), MO(_id -> 1)) != None

  }

  trait Node extends SingleOpsable with Attrable with Nodeable with HasId[String]

  trait Relationship extends SingleOpsable with Attrable with Relationable with HasId[String] {
    def endNode: Node
    def startNode: Node
    def nodes: (Node, Node)
    def otherNode(node: Node): Node
  }
  trait Relationable {
    def fromId: String
    def toId: String
    def rsType: String
  }

  trait RelationshipImpl extends Relationship with SingleOpsable with AttrableImpl with RSTpl with Relationable {

    def nodes: (Node, Node) = (startNode, endNode)
    lazy val defaultMO = MO(_id -> id, "e" -> Relationship.LeftId(endNode.id, rsType))
    def otherNode(node: Node): Node =
      if (startNode == node) endNode else startNode

  }

  object Node {
    def apply(uid: String, isLazy: Boolean = true) =
      if (isLazy)
        new NodeableImpl with LazyMoData {
          val id = uid
        }
      else new NodeableImpl with NoExistsMoData {
        val id = uid
      }

    def apply(mobj: DBObject) = new NodeableImpl with IntimeMoData {
      def mo = mobj
    }
  }

  object Relationship {
    object RightId {
      def apply(fromId: String, rs: String, toId: String) =
        fromId + ">" + rs + ">" + toId
      def head(fromId: String) = fromId + ">"
      def init(fromId: String, rs: String) =
        fromId + ">" + rs + ">"

      def unapply(id: String) = {
        Try {
          val Array(from, rs, to) = id.split(">")
          (from, rs, to)
        }.toOption
      }
    }

    object LeftId {
      def apply(toId: String, rs: String) =
        toId + "<" + rs
      def head(toId: String) =
        toId + "<"
      def unapply(id: String) =
        Try {
          val Array(to, rs) = id.split("<")
          (to, rs)
        }.toOption
    }

    def apply(mobj: DBObject): Relationship =
      new RelationshipImpl with IntimeMoData {
        def mo = mobj
        val RightId(fromId, rsType, toId) = mobj.id[String]
        val startNode = Node(fromId, true)
        val endNode = Node(toId, true)
      }

    def apply(start: Node, rs: String, end: Node): Relationship =
      new RelationshipImpl with LazyMoData {
        val fromId = start.id
        val toId = end.id
        val rsType = rs
        val id = Relationship.RightId(start.id, rs, end.id)
        val startNode = start
        val endNode = end
      }

    def apply(fromId: String, rs: String, toId: String): Relationship =
      apply(Node(fromId, true), rs, Node(toId, true))

    def apply(tupleId: (String, String, String)): Relationship = {
      val (f, r, t) = tupleId
      apply(f, r, t)
    }

  }

}
